/*
 * Copyright 2002-2010 Edward L. Stauff.                  Contact: <EdStauff@gmail.com>.
 * 
 * This file, "FileSystemPath.cs", is part of the "Stauffware.Common" class library.
 * "Stauffware.Common" is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser Public License as published by the 
 * Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common" is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * the "Stauffware.Common" sources.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using Stauffware.Common.Core;

namespace Stauffware.Common.IO
{
	/// <summary>
	/// An immutable object representing a file system path.
	/// </summary>
	public class FileSystemPath : IComparable
	{
		#region Static Fields & Properties

		/// <summary>
        /// Used for the creation of new FileSystemPaths.
        /// Changing this variable does *NOT* affect existing FileSystemPaths.
        /// </summary>
		public static char DefaultDirectorySeparator = '\\';

		/// <summary>
		/// The valid wildcard characters.
		/// </summary>
		public static readonly char[] WildcardChars = "*?".ToCharArray();

		#endregion

		#region Static Methods

		//=====================================================================
		/// <summary>
		/// Normalizes the given path by removing "." and extraneous "..", and 
		/// making directory separators consistent.  
		/// Does *NOT* convert a relative path to a fully qualified path.
		/// </summary>

		public static String NormalizePath (String path, char dirSepChar)
		{
			string dirSep = "" + dirSepChar;

			if (dirSepChar != '/')
				path = path.Replace('/', dirSepChar);
			if (dirSepChar != '\\')
				path = path.Replace('\\', dirSepChar);

			if (path.EndsWith(dirSep) && !path.EndsWith(":" + dirSep))
				path = path.Substring(0, path.Length - 1);

			//	path = path.Replace("." + dirSep, "");
			//	path = path.Replace(dirSep + ".", "");

			path = path.Replace(dirSep + "." + dirSep, dirSep);
			if (path.StartsWith("." + dirSep))
				path = path.Substring(2);
			if (path.EndsWith(dirSep + "."))
				path = path.Substring(0, path.Length - 2);

			List<string> parts = new List<string>(path.Split(dirSep.ToCharArray()));
			int i = 1;
			while (i < parts.Count)
			{
				if (parts[i] == ".." && parts[i-1] != "..")
				{
					parts.RemoveRange(i-1, 2);
					if (i > 0)
						i--;
				}
				else
					i++;
			}


			//for (int i = parts.Length - 1; i > 0; i--)
			//{
			//    if (parts[i] == "..")
			//    {
			//        parts[i] = "\t";
			//        parts[i - 1] = "\t";
			//    }
			//}

			path = String.Join(dirSep, parts);
		//	path = path.Replace(dirSep + "\t" + dirSep + "\t", "");
		//	path = path.Replace("\t" + dirSep + "\t" + dirSep, "");
			return path;
		}

		//=====================================================================
		/// <summary>
		/// Removes the terminal component from the given path and returns the
		/// resulting path (i.e. the directory portion).
		/// </summary>
		/// <remarks>
		/// This is useful for splitting off the directory portion of a path
		/// with wildcards, because stupid Path.GetFullPath chokes on wildcards.
		/// </remarks>

		public static string RemoveTerminalComponent (string path, out string terminalComponent)
		{
			int n = path.LastIndexOfAny("/\\:".ToCharArray());
			if (n < 0)
			{
				terminalComponent = path;
				return ".";
			}
			else if (n == path.Length - 1)
			{
				terminalComponent = "";
				return path;
			}
			else
			{
				terminalComponent = path.Substring(n + 1);
				return path.Substring(0, n + 1);
			}
		}

		//=====================================================================
		/// <summary>
		/// Creates a temporary file with (hopefully) a name that won't collide
		/// with some other file.  An empty file is created.
		/// </summary>
		/// <param name="prefix">a prefix for the file name, or null</param>
		/// <param name="suffix">a suffix for the file name (such as an extension), or null</param>
		/// <param name="dir">the directory in which to create the file</param>

		public static FileStream CreateTemporaryFile (string prefix, string suffix, string dir)
		{
			const string digits = "0123456789abcdefghijklmnopqrstuvwxyz";
			if (prefix == null)
				prefix = "";
			if (suffix == null)
				suffix = "";

			DateTime now = DateTime.Now;

			TimeSpan ts = now - (new DateTime(now.Year, 1, 1));

			long number = (long) ts.TotalMilliseconds;
		// What a crock.  Fix this if you have a better idea.
#if SILVERLIGHT
			prefix += NumUtils.ToString(Random.Next(), digits);
#else
			prefix += NumUtils.ToString(System.Diagnostics.Process.GetCurrentProcess().Id, digits);
#endif
			int giveUp = 100;

			while (giveUp-- > 0)
			{
				string name = Path.Combine(dir, prefix + NumUtils.ToString(number, digits) + suffix);
				try
				{
					return File.Create(name);
				}
				catch (IOException)
				{
					number++;
				}
			}

			throw new Exception("CreateTemporaryFile bailing after 100 tries");
		}

#if SILVERLIGHT
		private static readonly Random Random = new Random((int) DateTime.Now.Ticks & 0x7FFFFFFF);
#endif
		//=====================================================================
		/// <summary>
		/// Returns whether the given path contains any wildcard characters.
		/// </summary>

		public static bool ContainsWildcards (string path)
		{
			return path.IndexOfAny(WildcardChars) >= 0;
		}

		//=====================================================================
#if SILVERLIGHT
		/// <summary>
		/// <b>On Silverlight, this method returns its unmodified input string.</b>
		/// Here's what it's supposed to do:<br/>
		/// For each pair of percent signs ('%') in the given path, the text
		/// between them is taken to be the name of an environment variable,
		/// which is replaced (along with the percent signs) with the value of
		/// the environment variable.  Case is ignored.
		/// Two adjacent percent signs "%%" are replaced with a single one "%".
		/// Throws an exception if a specified environment variable is undefined.
		/// </summary>

		[Obsolete("Silverlight does not support special folders.  " +
				  "This method will return the unmodified input string.")]
		public static string ResolveEnvironmentVariables (string path)
		{
			return path;
		}

#else
		/// <summary>
		/// For each pair of percent signs ('%') in the given path, the text
		/// between them is taken to be the name of an environment variable,
		/// which is replaced (along with the percent signs) with the value of
		/// the environment variable.  Case is ignored.
		/// Two adjacent percent signs "%%" are replaced with a single one "%".
		/// Throws an exception if a specified environment variable is undefined.
		/// </summary>
		public static string ResolveEnvironmentVariables (string path)
		{
			return ResolveVariables(path, (name) =>
			{ return Environment.GetEnvironmentVariable(name); });
		}
#endif
		//---------------------------------------------------------------------
#if SILVERLIGHT
		/// <summary>
		/// <b>On Silverlight, this method returns its unmodified input string.</b>
		/// Here's what it's supposed to do:<br/>
		/// For each pair of percent signs ('%') in the given path, the text between
		/// them is taken to be the name of a System.Environment.SpecialFolder enum
		/// value, which is replaced (along with the percent signs) with the return
		/// value from System.Environment.GetFolderPath().
		/// Two adjacent percent signs "%%" are replaced with a single one "%".
		/// </summary>
		[Obsolete("Silverlight does not support special folders.  " + 
				  "This method will return the unmodified input string.")]
		public static string ResolveSpecialFolders (string input, object option = null)
		{
			return input;
		}
#else
		/// <summary>
		/// For each pair of percent signs ('%') in the given path, the text between
		/// them is taken to be the name of a System.Environment.SpecialFolder enum
		/// value, which is replaced (along with the percent signs) with the return
		/// value from System.Environment.GetFolderPath().
		/// Two adjacent percent signs "%%" are replaced with a single one "%".
		/// </summary>
		/// <param name="input">the text to be processed</param>
		/// <param name="option">
		/// This is passed to System.Environment.GetFolderPath().
		/// If not specified, it defaults to DoNotVerify.
		/// </param>
		/// <returns>the processed text</returns>
		public static string ResolveSpecialFolders (string input,
			Environment.SpecialFolderOption option = Environment.SpecialFolderOption.DoNotVerify)
		{
			return ResolveVariables(input, (name) =>
			{
				Environment.SpecialFolder sf;
				if (Enum.TryParse<Environment.SpecialFolder>(name, true, out sf))
					return Environment.GetFolderPath(sf, option);
				else return null;
			});
		}
#endif
		//---------------------------------------------------------------------
		// Helper for ResolveEnvironmentVariables and ResolveSpecialFolders.

		private static string ResolveVariables (string path, Func<string, string> resolver)
		{
			ListString badVars = new ListString("\"", ", ", "\"");
			string varName;
			int offset = 0;
			while (null != (varName = GetNextVariable(ref path, ref offset)))
			{
				string value = resolver(varName);
				if (value == null)
					badVars.Add(varName);
				else
				{
					path = path.Insert(offset, value);
					offset += value.Length;
				}
			}

			if (badVars.Count > 0)
				throw new Exception("undefined variable name(s) in path: " + 
									badVars.ToString()).AddData("path", path);
			return path;
		}

		//---------------------------------------------------------------------
		// Scans the given input string, starting at the given offset, for the
		// first two occurrences of "%", removes the two "%"s and everything 
		// between, updates the offset to indicate the removed text, and returns 
		// the characters between the "%"s.
		// If it only finds one "%", it throws an exception.  If it doesn't find
		// a "%", it sets offset to be the length of the input and returns null.

		private static string GetNextVariable (ref string input, ref int offset)
		{
			string retStr;

		//	string originalInput = input;
		//	int originalOffset = offset;

			for(;;)
			{
				int start = input.IndexOf('%', offset);
				if (start < 0)
				{
				//	offset = input.Length;
					offset = start;
					retStr = null;
					break;
				}

				int end = input.IndexOf('%', start+1);
				if (end < 0)
					throw new Exception("unmatched '%' in path").AddData("path", input);

				int retLen = (end - start) - 1;  // not including "%"s
				if (retLen > 0)
				{
					retStr = input.Substring(start + 1, retLen);
					input = input.Remove(start, retLen + 2);
					offset = start;
					//if (input.Length > 0)
					//    offset += 1;
					break;
				}

				input = input.Remove(start, 1); // remove one "%", leave the other
				offset = start + 1;
			}

		//	System.Console.WriteLine("GetNextVariable(\"" + originalInput + "\" -> \"" +
		//							input + "\", " + originalOffset.ToString() + " -> " +
		//							offset.ToString() + ") -> \"" + retStr + "\"" );
			return retStr;
		}

		//=====================================================================
		/// <summary>
		/// Works just like System.IO.Path.GetFullPath(), except that it doesn't
		/// choke on wildcards.
		/// </summary>

		public static string GetFullPath (string path)
		{
			int wc = path.IndexOfAny(WildcardChars);
			if (wc < 0)
				return Path.GetFullPath(path);

			string notSafe = path.Substring(wc);
			string safe = path.Substring(0, wc);
			if (safe == "")
				safe = ".";

			return Path.GetFullPath(safe) + notSafe;
		}

		//=====================================================================

		#endregion

		#region Instance Fields & Properties

		/// <summary>
		/// The normalized path name (not necessarily rooted).
		/// Redundant "." and ".." directories have been removed.
		/// </summary>
		public readonly String PathName;

		/// <summary>
		/// The directory separator character used in this path.
		/// </summary>
		public readonly char DirectorySeparator;

		/// <summary>
		/// Returns whether the path is relative.  Always equal to ! IsAbsolute.
		/// </summary>
		public readonly bool IsRelative;

		/// <summary>
		/// Returns whether the path is absolute.  Always equal to ! IsRelative.
		/// </summary>
		public bool IsAbsolute { get { return !this.IsRelative; } }

		//---------------------------------------------------------------------
		/// <summary>
		/// The terminal name in the path; that is, the file name (if the path denotes a file).
		/// </summary>

		public String TerminalName
		{
			get
			{
				if (this._terminalName == null)
				{
					int n = this.PathName.LastIndexOfAny((":" + this.DirectorySeparator).ToCharArray());
					if (n < 0)
						this._terminalName = this.PathName;
					else this._terminalName = this.PathName.Substring(n + 1);
				}
				return this._terminalName;
			}
		}
		private String _terminalName = null;

		//---------------------------------------------------------------------
		/// <summary>
		/// The (final) extension on the terminal name, NOT including the dot.
		/// Returns an empty string if there's no extension.
		/// </summary>

		public String TerminalExtension
		{
			get
			{
				if (this._extension == null)
				{
					int n = this.TerminalName.LastIndexOf(".");
					if (n < 0)
						this._extension = "";
					else this._extension = this.TerminalName.Substring(n + 1);
				}
				return this._extension;
			}
		}
		private String _extension = null;

		//---------------------------------------------------------------------
		/// <summary>
		/// The terminal name, minus its (final) extension, if any.
		/// If the terminal name has multiple extensions, only the final one
		/// is removed; e.g. "a.b.c" -> "a.b".
		/// </summary>

		public String TerminalNameWithoutExtension
		{
			get
			{
				if (this._nameMinusExt == null)
				{
					int n = this.TerminalName.LastIndexOf(".");
					if (n < 0)
						this._nameMinusExt = this.TerminalName;
					else this._nameMinusExt = this.TerminalName.Substring(0, n);
				}
				return this._nameMinusExt;
			}
		}
		private String _nameMinusExt = null;

		//---------------------------------------------------------------------
		/// <summary>
		/// The parent directory of this file system object.
		/// If no parent directory was specified, returns null.
		/// </summary>

		public String ParentDirectory
		{
			get
			{
				if (!this._gotParentDir)
				{
					int n = this.PathName.LastIndexOf(this.DirectorySeparator);
					if (n > 0)
						this._parentDir = this.PathName.Substring(0, n);
					this._gotParentDir = true;
				}
				return this._parentDir;
			}
		}
		private String _parentDir = null;
		private bool _gotParentDir = false;

		//---------------------------------------------------------------------
		/// <summary>
		/// The system name of this file system object.
		/// If no system name was specified, returns null.
		/// </summary>

		public String SystemName
		{
			get
			{
				if (!this._gotSystem)
				{
					// count the number of leading directory separator characters
					int i;
					for (i = 0; i < PathName.Length; i++)
						if (this.PathName[i] != this.DirectorySeparator)
							break;
					if (i >= 2)		// path starts with at least two dir separators
					{
						this._system = this.PathName.Substring(i);
						i = this._system.IndexOf(this.DirectorySeparator);
						if (i > 0)
							this._system = this._system.Substring(0, i);
					}
					this._gotSystem = true;
				}
				return this._system;
			}
		}
		private String _system = null;
		private bool _gotSystem = false;

		#endregion

		#region Constructors

		//=====================================================================
		/// <summary>
		///  constructor, using DefaultDirectorySeparator
		/// </summary>
		/// <param name="path">an absolute or relative path</param>

		public FileSystemPath (String path)
			: this(path, DefaultDirectorySeparator)
		{
		}

		//=====================================================================
		/// <summary>
		///  constructor
		/// </summary>
		/// <param name="path">an absolute or relative path</param>
		/// <param name="directorySeparator">the directory separator character</param>

		public FileSystemPath (String path, char directorySeparator)
		{
			Require.NotNull(path, "bogus null path");
			path = path.Trim();
			Require.Condition(path != "", "bogus empty path");

			this.DirectorySeparator = directorySeparator;
			this.PathName = NormalizePath(path, directorySeparator);

			if (this.PathName.Length < 2)
				this.IsRelative = true;
			else if (this.PathName[1] == ':')
				this.IsRelative = false;
			else if ((this.PathName[0] == directorySeparator) &&
					 (this.PathName[1] == directorySeparator))
				this.IsRelative = false;
			else this.IsRelative = true;
		}

		//=====================================================================

		#endregion

		#region Parts Methods

		//=====================================================================
		/// <summary>
		/// Returns an array containing the parts (directories) of the path.
		/// </summary>

		public List<string> GetParts ()
		{
			string[] parts = this.PathName.Split((":" + this.DirectorySeparator).ToCharArray());
			List<string> goodParts = new List<string>(parts.Length);
			foreach (string p in parts)
				if (!string.IsNullOrWhiteSpace(p))
					goodParts.Add(p);
			return goodParts;
		}

		//=====================================================================
		/// <summary>
		/// Returns the name of the parent directory; that is, the path minus
		/// the terminal name.
		/// </summary>

		public String GetParentDirectoryName ()
		{
			int n = this.PathName.LastIndexOf(this.DirectorySeparator);
			if (n < 0) return "";
			else return this.PathName.Substring(0, n);
		}

		//=====================================================================
		/// <summary>
		/// Returns a path equivalent to this path, relative to the given path.
		/// Returns null if the paths are equal.
		/// </summary>

		public FileSystemPath GetRelativeTo (FileSystemPath relativeToPath)
		{
			Require.Condition(this.IsAbsolute);
			Require.Condition(relativeToPath.IsAbsolute);
			if (this == relativeToPath) return null;

			List<string> thisParts = this.GetParts();
			List<string> thatParts = relativeToPath.GetParts();

			List<string> newParts = new List<string>();

			int min = Math.Min(thisParts.Count, thatParts.Count);

			// see how much matches up
			int match;
			for (match = 0; match < min; match++)
				if (String.Compare(thisParts[match], thatParts[match], 
							StringComparison.InvariantCultureIgnoreCase) != 0)
					break;

			for (int i = match; i < thatParts.Count; i++)
				newParts.Add("..");
			for (int i = match; i < thisParts.Count; i++)
				newParts.Add(thisParts[i]);

			StringBuilder newPath = new StringBuilder(relativeToPath.PathName.Length);
			foreach (String s in newParts)
			{
				newPath.Append(s);
				newPath.Append(this.DirectorySeparator);
			}

			// BUG: doesn't explicitly handle "//", ":"

			return new FileSystemPath(newPath.ToString());
		}

		//=====================================================================
		/// <summary>
		/// Returns whether this path contains the given sub-path.
		/// </summary>

		public bool Contains (FileSystemPath that)
		{
			String thisPath = this.PathName.ToLower() + this.DirectorySeparator;
			String thatPath = that.PathName.ToLower();
			if (this.DirectorySeparator != that.DirectorySeparator)
				thatPath = thatPath.Replace(that.DirectorySeparator, this.DirectorySeparator);
			return thatPath.StartsWith(thisPath);
		}

		//=====================================================================

		#endregion

		#region Miscellaneous methods

		/// <summary>
		/// Queries the file system to determine whether the path denotes an 
		/// existing file.
		/// </summary>
		public bool IsFile ( ) { return File.Exists(this.PathName); }

		/// <summary>
		/// Queries the file system to determine whether the path denotes an 
		/// existing directory.
		/// </summary>
		public bool IsDirectory ( ) { return Directory.Exists(this.PathName); }

		/// <summary>
		/// Queries the file system to determine whether the path denotes an 
		/// existing file or directory.
		/// </summary>
		public bool Exists ( ) { return this.IsFile() || this.IsDirectory(); }

		/// <summary>
		/// Returns whether this path contains any wildcards.
		/// </summary>
		public bool ContainsWildcards ( ) { return ContainsWildcards(this.PathName); }

		#endregion

		#region Operators

		//=====================================================================
		/// <summary>
		/// Concatenates two paths.  The second path must be a relative path.
		/// </summary>

		public static FileSystemPath operator + (FileSystemPath p1, FileSystemPath p2)
		{
			Assert.Condition(p2.IsRelative);
			return new FileSystemPath(p1.PathName + DefaultDirectorySeparator + p2.PathName);
		}

		//=====================================================================
		/// <summary>
		/// Concatenates two paths.  The second path must be a relative path.
		/// </summary>

		public static FileSystemPath operator + (FileSystemPath path, String str)
		{
			return path + new FileSystemPath(str, path.DirectorySeparator);
		}

		//=====================================================================
		/// <summary>
		/// Tests two paths for equality.
		/// </summary>

		public static bool operator == (FileSystemPath p1, FileSystemPath p2)
		{
			return p1.PathName.ToLower() == p2.PathName.ToLower();
		}

		//=====================================================================
		/// <summary>
		/// Tests two paths for equality.
		/// </summary>

		public static bool operator != (FileSystemPath p1, FileSystemPath p2)
		{
			return p1.PathName.ToLower() != p2.PathName.ToLower();
		}

		//=====================================================================

		#endregion

		#region Overrides & Interface Implementations

		//=====================================================================

        /// <summary></summary>
        /// <param name="obj"></param>
        /// <returns></returns>
		public override bool Equals (object obj)
		{
			FileSystemPath that = obj as FileSystemPath;
			if (null == (object) that) return false;
			return this.PathName.ToLower() == that.PathName.ToLower();
		}

		//=====================================================================

        /// <summary></summary>
        /// <returns></returns>
		public override string ToString ()
		{
			return this.PathName;
		}

		//=====================================================================

        /// <summary></summary>
        /// <returns></returns>
		public override int GetHashCode ()
		{
			return this.PathName.GetHashCode();
		}

		//=====================================================================

        /// <summary></summary>
        /// <param name="obj"></param>
        /// <returns></returns>
		public int CompareTo (object obj)
		{
			FileSystemPath that = obj as FileSystemPath;
			if (that == null) return -1;
			return String.Compare(this.PathName, that.PathName, StringComparison.InvariantCultureIgnoreCase);
		}

		//=====================================================================

		#endregion
	}

}